#include <zenoui/Game.hpp>
#include <zenoui/Widget.hpp>
#include <zenoui/Event.hpp>
#include <zenoui/check_gl.hpp>
#include <zenoui/FPSCounter.hpp>
#include <zenoui/FontData.hpp>
#include <zenoui/ImageIO.hpp>
#include <zenox/minilog.hpp>
#include <zenox/home.hpp>
#include <zenox/glm.hpp>
#include <iostream>

namespace zenoui {

namespace {

template <class, class ...Ts>
static void (*_impl_glfw_input_callback(void (Game::*pFn)(Ts...)))(GLFWwindow *, Ts...) {
    static void (Game::*gpFn)(Ts...);
    gpFn = pFn;
    return [] (GLFWwindow *window, Ts ...args) -> void {
        auto game = (Game *)glfwGetWindowUserPointer(window);
        if (game) [[likely]] {
            (game->*gpFn)(args...);
        }
    };
}

template <class FpFn>
static auto _glfw_input_callback(FpFn fpFn) {
    return _impl_glfw_input_callback<FpFn>(fpFn());
}

static MouseButton _glfw_get_pressed_mouse_buttons(GLFWwindow *window) {
    MouseButton btns = MouseNoButton;
    if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS)
        btns |= MouseLeftButton;
    if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS)
        btns |= MouseMiddleButton;
    if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS)
        btns |= MouseRightButton;
    return btns;
}

static MouseButton _glfw_get_mouse_button_enum(int button) {
    MouseButton btns = MouseNoButton;
    switch (button) {
    case GLFW_MOUSE_BUTTON_LEFT:
        btns = MouseLeftButton;
        break;
    case GLFW_MOUSE_BUTTON_MIDDLE:
        btns = MouseMiddleButton;
        break;
    case GLFW_MOUSE_BUTTON_RIGHT:
        btns = MouseRightButton;
        break;
    };
    return btns;
}

static KeyModifiers _glfw_get_modifiers_enum(int mods) {
    KeyModifiers m = NoModifiers;
    if (mods & GLFW_MOD_CONTROL) m |= ModifierCtrl;
    if (mods & GLFW_MOD_SHIFT) m |= ModifierShift;
    if (mods & GLFW_MOD_ALT) m |= ModifierAlt;
    return m;
}

}

struct Game::Private {
    GLFWwindow *glfw_window = nullptr;
    zenox::shared_ptr<Widget> root_widget;
    zenox::weak_ptr<Widget> accepted_widget = nullptr;
    zenox::weak_ptr<Widget> hovered_widget = nullptr;
    glm::vec2 last_cursor_pos;
    bool has_last_cursor_pos = false;
    bool cursor_clamped = false;
    FontCache font_cache;
    FPSCounter fps;
};

Game::Game() = default;
Game::~Game() = default;

Game &Game::instance() {
    static Game game;
    return game;
}

Widget *Game::set_root_widget(zenox::shared_ptr<Widget> widget) {
    auto widget_ptr = widget.get();
    m_private->root_widget = std::move(widget);
    int width, height;
    glfwGetWindowSize(m_private->glfw_window, &width, &height);
    framebuffer_size_callback(width, height);
    return widget_ptr;
}

FontCache *Game::get_font_cache() const {
    return &m_private->font_cache;
}

GLFWwindow *Game::get_glfw_window() const {
    return m_private->glfw_window;
}

void Game::set_glfw_window(GLFWwindow *window) {
    m_private->glfw_window = window;
    glfwSetWindowUserPointer(window, this);

    glfwSetCursorPosCallback(window, _glfw_input_callback([] { return &Game::cursor_pos_callback; }));
    glfwSetMouseButtonCallback(window, _glfw_input_callback([] { return &Game::mouse_button_callback; }));
    glfwSetScrollCallback(window, _glfw_input_callback([] { return &Game::scroll_callback; }));
    glfwSetKeyCallback(window, _glfw_input_callback([] { return &Game::key_callback; }));
    glfwSetCharCallback(window, _glfw_input_callback([] { return &Game::char_callback; }));
    glfwSetDropCallback(window, _glfw_input_callback([] { return &Game::drop_callback; }));
    glfwSetFramebufferSizeCallback(window, _glfw_input_callback([] { return &Game::framebuffer_size_callback; }));

    int width, height;
    glfwGetWindowSize(m_private->glfw_window, &width, &height);
    framebuffer_size_callback(width, height);

    Image<glm::u8vec4> icon(zenox::zeno_home_relative("assets/logo.png"));
    GLFWimage glfwicon;
    glfwicon.width = icon.sizex();
    glfwicon.height = icon.sizey();
    glfwicon.pixels = (unsigned char *)icon.data();
    glfwSetWindowIcon(m_private->glfw_window, 1, &glfwicon);
}

void Game::show_win() {
    glfwShowWindow(m_private->glfw_window);
}

bool Game::main_loop() {
    if (glfwWindowShouldClose(m_private->glfw_window)) [[unlikely]] {
        m_private = nullptr;
        glfwTerminate();
        return false;
    }

    // Render graphics
    render_gl();
    // Update screen
    m_private->fps.non_duty_begin();
    glfwSwapBuffers(m_private->glfw_window);
    glfwWaitEventsTimeout(1 / 24.0);
    /* glfwPollEvents(); */
    m_private->fps.non_duty_end();
    // Compute FPS
    if (m_private->fps.update_counter()) {
        glfwSetWindowTitle(m_private->glfw_window,
                           zenox::format("ZENO ({:.01f} FPS {:.00f} %)",
                                         m_private->fps.fps, m_private->fps.duty_ratio).c_str());
    }

    return true;
}

bool Game::init(int version, int width, int height, int numsamples, int framerate, bool fullscreen, bool osmesa) {
    // Initalize GLFW library
    if (!glfwInit()) {
        check_gl::opengl_show_glfw_error_diagnose("initialize");
        return false;
    }

    // Hint the version required
    glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
    glfwWindowHint(GLFW_CONTEXT_CREATION_API, osmesa ? GLFW_OSMESA_CONTEXT_API : GLFW_NATIVE_CONTEXT_API);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, version / 10);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, version % 10);
    glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
    if (version >= 33) {
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
#ifdef __APPLE__
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
    }

    // enable 4x MSAA
    glfwWindowHint(GLFW_SAMPLES, numsamples);

    // enable sRGB
    glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE);

    // lock to 60 FPS
    if (framerate >= 0)
        glfwWindowHint(GLFW_REFRESH_RATE, framerate);

    // Enable transparent framebuffer
    /* glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); */
    /* glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); */
    /* glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); */
    /* glfwWindowHint(GLFW_FLOATING, GLFW_TRUE); */
    /* glfwWindowHint(GLFW_FOCUS_ON_SHOW, GLFW_FALSE); */
    /* glfwWindowHint(GLFW_FOCUSED, GLFW_FALSE); */

    // Create main window
    GLFWwindow *window = glfwCreateWindow(width, height, "ZENO", NULL, NULL);

    // Test if window creation succeed
    if (!window) {
        check_gl::opengl_show_glfw_error_diagnose("create window");
        glfwTerminate();
        return false;
    }

    // Switch to fullscreen mode
    if (!osmesa) {
        if (fullscreen) {
            GLFWmonitor *monitor = glfwGetPrimaryMonitor();
            if (monitor) {
                const GLFWvidmode *mode = glfwGetVideoMode(monitor);
                if (mode) {
                    glfwSetWindowSize(window, mode->width, mode->height);
                    glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate);
                    zenox::log_info() << "Entered fullscreen mode: " << mode->width << 'x' << mode->height
                        << " at " << mode->refreshRate << " Hz";
                }
            }
        } else {
            GLFWmonitor *monitor = glfwGetPrimaryMonitor();
            if (monitor) {
                const GLFWvidmode *mode = glfwGetVideoMode(monitor);
                if (mode) {
                    int width, height;
                    glfwGetWindowSize(window, &width, &height);
                    glfwSetWindowPos(window, (mode->width - width) / 2, (mode->height - height) / 2);
                }
            }
        }
    }

    glfwMakeContextCurrent(window);

    // Load glXXX function pointers (only after this you may use OpenGL functions)
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        zenox::log_error("GLAD failed to load OpenGL functions");
        glfwTerminate();
        return false;
    }
    check_gl::opengl_try_enable_debug_message();

    // Print diagnostic information
    zenox::log_info("OpenGL version: {}", (const char *)glGetString(GL_VERSION));

    m_private = std::make_unique<Private>();

    // Create game instance
    set_glfw_window(window);

    // Setup OpenGL context
    setup_gl();

    return true;
}

void Game::setup_gl() {
    CHECK_GL(glEnable(GL_MULTISAMPLE));
    CHECK_GL(glEnable(GL_BLEND));
    CHECK_GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
    CHECK_GL(glEnable(GL_TEXTURE_2D));
    /* CHECK_GL(glEnable(GL_FRAMEBUFFER_SRGB)); */
    /* CHECK_GL(glEnable(GL_CULL_FACE)); */
    /* CHECK_GL(glCullFace(GL_BACK)); */
    /* CHECK_GL(glFrontFace(GL_CW)); */
    CHECK_GL(glEnable(GL_SCISSOR_TEST));
}

ZENO_HOT_FN void Game::render_gl() {
    int width, height;
    glfwGetWindowSize(m_private->glfw_window, &width, &height);
    CHECK_GL(glViewport(0, 0, width, height));
    CHECK_GL(glClear(GL_COLOR_BUFFER_BIT));

    CHECK_GL(glPushAttrib(GL_ALL_ATTRIB_BITS));
    if (m_private->root_widget) [[likely]] {
        Transform xform{};
        Rect rootrect{{0, 0}, {width, height}};
        int viewdims[4] = {0, 0, width, height};
        m_private->root_widget->do_paint(rootrect, xform, viewdims);
    }
    CHECK_GL(glPopAttrib());

#ifndef NDEBUG
    if (std::getenv("ZENO_SHOW_DEBUG_RECT") && m_private->root_widget) [[unlikely]] {
        m_private->root_widget->paint_debug_rect();
    }
#endif
}

ZENO_HOT_FN void Game::cursor_pos_callback(double xpos, double ypos) {
    int width, height;
    glfwGetWindowSize(m_private->glfw_window, &width, &height);

    auto pressed_btns = _glfw_get_pressed_mouse_buttons(m_private->glfw_window);

    /* if (m_private->accepted_widget && !m_private->accepted_widget->rect().contains({xpos, ypos})) { */
    /*     // clamp mouse cursor inside the window (ZHI JING Blender) */
    /*     auto rect = m_private->accepted_widget->rect(); */
    /*     xpos = std::fmod(xpos - rect.topleft().x + rect.size().x, rect.size().x) + rect.topleft().x; */
    /*     ypos = std::fmod(ypos - rect.topleft().y + rect.size().y, rect.size().y) + rect.topleft().y; */
    /*     glfwSetCursorPos(m_private->glfw_window, xpos, ypos); */
    /*     m_private->cursor_clamped = true; */
    /* } else { */
    /*     m_private->cursor_clamped = false; */
    /* } */
    glm::vec2 pos(xpos, ypos);
    {
        MouseHoverEvent hoverEvent;
        hoverEvent.m_pos = pos;
        hoverEvent.m_hover_type = MouseHoverEvent::HoverDetect;
        auto new_hovered_widget = dispatch_event_to_child(&hoverEvent);
        if (new_hovered_widget != m_private->hovered_widget) {
            MouseHoverEvent leaveEvent;
            leaveEvent.m_pos = pos;
            leaveEvent.m_hover_type = MouseHoverEvent::HoverLeave;
            if (m_private->hovered_widget) [[likely]]
                m_private->hovered_widget->do_direct_handle(&leaveEvent);
            MouseHoverEvent enterEvent;
            enterEvent.m_pos = pos;
            enterEvent.m_hover_type = MouseHoverEvent::HoverEnter;
            if (new_hovered_widget) [[likely]]
                new_hovered_widget->do_direct_handle(&enterEvent);
            m_private->hovered_widget = new_hovered_widget;
        }
    }

    if (!m_private->has_last_cursor_pos) [[unlikely]]
        m_private->last_cursor_pos = pos;
    auto delta_pos = pos - m_private->last_cursor_pos;
    {
        MouseMoveEvent moveEvent;
        moveEvent.m_pos = pos;
        moveEvent.m_delta_pos = delta_pos;
        if (pressed_btns && m_private->accepted_widget) {
            moveEvent.m_button = pressed_btns;
            m_private->accepted_widget->do_direct_handle(&moveEvent);
        } else {
            moveEvent.m_button = MouseNoButton;
            m_private->accepted_widget = dispatch_event_to_child(&moveEvent);
        }
    }
    m_private->last_cursor_pos = pos;
    m_private->has_last_cursor_pos = true;
}

ZENO_HOT_FN static glm::vec2 _glfw_get_mouse_pos(GLFWwindow *window) {
    double xpos, ypos;
    glfwGetCursorPos(window, &xpos, &ypos);
    glm::vec2 pos((float)xpos, (float)ypos);
    return pos;
}

ZENO_HOT_FN void Game::mouse_button_callback(int button, int action, int mods) {
    auto pos = _glfw_get_mouse_pos(m_private->glfw_window);
    m_private->last_cursor_pos = pos;
    m_private->has_last_cursor_pos = true;

    auto pressed_btns = _glfw_get_mouse_button_enum(button);
    auto pressed_mods = _glfw_get_modifiers_enum(mods);

    if (action == GLFW_PRESS) {
        MouseClickEvent clickEvent;
        clickEvent.m_pos = pos;
        clickEvent.m_button = _glfw_get_mouse_button_enum(button);
        clickEvent.m_mods = pressed_mods;
        m_private->accepted_widget = dispatch_event_to_child(&clickEvent);
    } else {
        MouseReleaseEvent relEvent;
        relEvent.m_pos = pos;
        relEvent.m_button = pressed_btns;
        relEvent.m_mods = pressed_mods;
        if (pressed_btns && m_private->accepted_widget) {
            m_private->accepted_widget->do_direct_handle(&relEvent);
        } else {
            dispatch_event_to_child(&relEvent);
        }
        m_private->accepted_widget = nullptr;
    }

    /* GLFWcursor *cursor = glfwCreateStandardCursor(m_private->accepted_widget ? GLFW_CROSSHAIR_CURSOR : GLFW_ARROW_CURSOR); */
    /* glfwSetCursor(m_private->glfw_window, cursor); */
}

ZENO_HOT_FN void Game::scroll_callback(double xoffset, double yoffset) {
    auto pos = _glfw_get_mouse_pos(m_private->glfw_window);
    glm::vec2 delta((float)xoffset, (float)yoffset);
    MouseWheelEvent whlEvent;
    whlEvent.m_pos = pos;
    whlEvent.m_delta = delta;
    m_private->accepted_widget = dispatch_event_to_child(&whlEvent);
}

void Game::drop_callback(int count, const char **paths) {
    auto pos = _glfw_get_mouse_pos(m_private->glfw_window);
    DragAndDropEvent dndEvent;
    dndEvent.m_pos = pos;
    dndEvent.m_paths.assign(paths, paths + count);
    zenox::log_critical("drop {} file(s) with path [{}] at mouse {}", count, zenox::join(dndEvent.m_paths, ":"), pos);
    m_private->accepted_widget = dispatch_event_to_child(&dndEvent);
}

void Game::char_callback(unsigned codeprint) {
    auto pos = _glfw_get_mouse_pos(m_private->glfw_window);
    CharInputEvent chEvent;
    chEvent.m_pos = pos;
    chEvent.m_code = (char32_t)codeprint;
    m_private->accepted_widget = dispatch_event_to_child(&chEvent);
}

void Game::key_callback(int key, int scancode, int action, int mods) {
    (void)scancode;

    auto pos = _glfw_get_mouse_pos(m_private->glfw_window);
    if (action == GLFW_PRESS) {
        KeyPressEvent preEvent;
        preEvent.m_pos = pos;
        preEvent.m_key = key;
        preEvent.m_mods = _glfw_get_modifiers_enum(mods);
        m_private->accepted_widget = dispatch_event_to_child(&preEvent);
    } else {
        KeyReleaseEvent relEvent;
        relEvent.m_pos = pos;
        relEvent.m_key = key;
        relEvent.m_mods = _glfw_get_modifiers_enum(mods);
        dispatch_event_to_child(&relEvent);
        m_private->accepted_widget = nullptr;
    }
}

void Game::framebuffer_size_callback(int width, int height) {
    // b = -1
    // aw + b = 1
    // aw + b = -1
    // b = 1
    CHECK_GL(glMatrixMode(GL_PROJECTION));
    glm::mat4x4 projection = glm::mat4x4(
        2.0f / width, 0.0f, 0.0f, 0.0f,
        0.0f, -2.0f / height, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        -1.0f, 1.0f, 0.0f, 1.0f);
    CHECK_GL(glLoadMatrixf(glm::value_ptr(projection)));
    CHECK_GL(glMatrixMode(GL_MODELVIEW));

    /* ResizeEvent resizeEvent; */
    /* resizeEvent.m_pos = glm::vec2(0, 0); */
    /* resizeEvent.m_new_rect.m_topleft = glm::vec2(0, 0); */
    /* resizeEvent.m_new_rect.m_size = glm::vec2(width, height); */
    /* dispatch_event_to_child(&resizeEvent); */
    if (m_private->root_widget) [[likely]]
        m_private->root_widget->update_rect(Rect{glm::vec2(0), glm::vec2(width, height)});
}

ZENO_HOT_FN zenox::weak_ptr<Widget> Game::dispatch_event_to_child(Event *event) {
    if (m_private->root_widget) [[likely]]
        m_private->root_widget->do_handle(event);
    return event->m_accepted;
}

}
